/*
* Copyright 2007-2008 WorldWide Conferencing, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions
* and limitations under the License.
*/

package net.liftweb.widgets.trees.treetable

import scala.xml.{NodeSeq, Node, PCData, Text, Unparsed, NodeBuffer, Elem}
import scala.collection.mutable.HashMap

import net.liftweb.widgets.trees.model.{TreeModel,NodeInfo,TtAjaxHelpers}
import net.liftweb.widgets.trees.util.{BinderMethod, TtAjaxBinder, JqAfter}

import java.lang.reflect.Method

import net.liftweb.http.S
import net.liftweb.http.S.SFuncHolder
import net.liftweb.http.LiftRules
import net.liftweb.util.Helpers._
import net.liftweb.http.js.JsCmd
import net.liftweb.http.js.JsCmds._
import net.liftweb.http.js.JE._
import net.liftweb.util.{Can, Full, Empty}


/*
 * 
 */
 
class TreeTableOptions {
  val showRoot : Boolean = false
  val collapseUnderLevel : Option [Int] = None  // None => don't collapse
  val striped : Boolean = false
  val highlight: Boolean = true
  val column: Int = 0
  val onselect : Option [String] = None
}

/*
 * 
 */
 
object TreeTable {
  /**
   * register the resources with lift (typically in boot)
   */
   
  def init() {  
    import net.liftweb.http.ResourceServer  
   
    ResourceServer.allow({  
      case "treetable" :: _ => true  
    })
    
    println("register tree-table")  
  }
  
  class FuncCallback [A <: AnyRef, B <: AnyRef] (val ttAjaxBinder : TtAjaxBinder) {
    var templates : HashMap [String, Node] = new HashMap ()
    
    def liftAjaxCallback (num : String) : JsCmd = {
      try {
        val id = num.substring (3, num.indexOf ("_pid_"))
        val _type = num.substring (num.indexOf ("_type_") + 6, num.indexOf ("_nextId_")) 
        val _context = num.substring (num.indexOf ("_context_") + 9) 
      
        val _num = num.substring (0, num.indexOf ("_context_"))
      
        //

        val imgs = for (ch <- stringWrapper (_context)) yield {
          if (ch == '0')
            <img src="/classpath/treetable/themes/default/blank.gif" class="preimg"/>
          else
            <img src="/classpath/treetable/themes/default/vertline.gif" class="preimg"/>
        }

        // println ("id:" + id + ", type:" + _type)
      
        val methods = ttAjaxBinder.getMethods (_type)

        val parametersFind = new Array [Object] (1)
        parametersFind (0) = id
          
        val tm = methods._4.invoke (methods._1.finder, parametersFind).asInstanceOf [TreeModel [AnyRef, AnyRef]]

        println ("TreeTableliftAjaxCallback # nodes" + tm.getLeafs.size)

        val zs = new NodeBuffer();
    
        renderList (tm.getLeafs, tm.getNode, None, imgs.toList.reverse, zs, ttAjaxBinder, templates)

        println ("TreeTableliftAjaxCallback xhtml to render:\n" + zs)

        // todo ver como convertir NodeBuffer a NodeSeq
        
        val jqCmd = JqId (_num) >> JqAfter (zs)
      
        println ("TreeTableliftAjaxCallback jQuery cmd:\n" + jqCmd)

        jqCmd
      }
      catch {
        case ex : Exception => {
          println ("TreeTableliftAjaxCallback error: ")
          ex.printStackTrace () ;
          Alert ("Error en el procesamiento: ver log en servidor")
        }
      }
    }
  }
  
  //

  private def renderTr (row : AnyRef, 
                        parent : AnyRef, 
                        next : Option [AnyRef],
                        zs : NodeBuffer, 
                        imgs : List [NodeSeq], 
                        imgNode : NodeSeq,
                        ttAjaxBinder : TtAjaxBinder,
                        templates : HashMap [String, Node]
  ) = {
    val rowType = BinderMethod.rowTypeKey (row.getClass)

    // println (rowType)
        
    try {
      // search the row template
      val template = templates (rowType)
            
      // search the row-binder function
      val methods = ttAjaxBinder.getMethods (rowType)

      val parametersBinder = new Array [Object] (2)
      parametersBinder (0) = template
      parametersBinder (1) = row
          
      // invoke the row binder
      val xhtmlVars = methods._2.invoke (methods._1.binder, parametersBinder)
          
      val xhtmlTr = bind ("tt", 
                        xhtmlVars.asInstanceOf [NodeSeq], 
                        "imgs" -> (<span>{imgs.reverse}{imgNode}</span>)) 

      // id of the row
      val idRow = getId (row, ttAjaxBinder)
      // id of the row¿s ´parent
      val idParent = getId (parent, ttAjaxBinder)

      val (nextId, nextType) = next match {
        case Some (_next) => (
            getId (_next, ttAjaxBinder),
            BinderMethod.rowTypeKey (_next.getClass)
          )
        case None => ("", "")
      }

      val rowId = ("id_" + idRow + "_pid_" + idParent + "_type_" + rowType + "_nextId_" + nextId + "_nextType_" + nextType)
          
      val xhtmlTrWithId = (xhtmlTr \\ "tr").first.asInstanceOf [Elem] % ("id" -> rowId)
            
      zs &+ xhtmlTrWithId
    }
    catch {
      case nseex : NoSuchElementException  => println ("Either template or binder not defined for row type " + rowType) ; nseex.printStackTrace 
    }
  }

  // id of the row
  private def getId (row : AnyRef, ttAjaxBinder : TtAjaxBinder) : String = {
    val rowType = BinderMethod.rowTypeKey (row.getClass)
        
    val methods = ttAjaxBinder.getMethods (rowType)
        
        
    val parametersId = new Array [Object] (1)
    parametersId (0) = row
          
    val idRow = methods._3.invoke (methods._1.id, parametersId)

    idRow.asInstanceOf [String]
  }
    
  //
  
  private def renderList [A <: AnyRef, B <: AnyRef] (
    tms : List [TreeModel[A,B]], 
    parent : AnyRef,
    next : Option [AnyRef],
    imgs : List [Node], 
    zs : NodeBuffer,
    ttAjaxBinder : TtAjaxBinder,
    templates : HashMap [String, Node]
  ) : Unit = {
    tms match {
      case head :: tail =>

        val rowType = BinderMethod.rowTypeKey (head.getNode.getClass)
        val rowId = getId (head.getNode, ttAjaxBinder)

        val imgId = "img_" + rowType + "_" + rowId
            
        if (head.isCollapsed) {
          val (imgNode, _next) = tail match {
            case head2 :: tail2 =>
              /* <div class="bg_not_last"></div> */
              (
                <img src="/classpath/treetable/themes/default/tv-expandable.gif" alt="" id={imgId} class="ajaximg"/>,
                Some (head2.getNode)
              )
            case Nil =>
              (
                <img src="/classpath/treetable/themes/default/tv-expandable-last.gif" alt="" id={imgId} class="ajaximg"/>,
                next
              )
          }
              
          renderTr (head.getNode, parent, _next, zs, imgs, imgNode, ttAjaxBinder, templates)
        }

        else if (head.getLeafs.size > 0) {
          val (imgNode, imgLeaf, _next) = tail match {
            case head2 :: tail2 =>
              (
                /* <div class="bg_not_last"></div> */
                <img src="/classpath/treetable/themes/default/tv-collapsable.gif" alt="" id={imgId} class="parimg"/>,
                <img src="/classpath/treetable/themes/default/vertline.gif" class="preimg"/>,
                Some (head2.getNode)
              )
            case Nil =>
              (
                <img src="/classpath/treetable/themes/default/tv-collapsable-last.gif" alt="" id={imgId} class="parimg"/>,
                <img src="/classpath/treetable/themes/default/blank.gif" class="preimg"/>,
                next
              )
            }

          renderTr (head.getNode, parent, _next, zs, imgs, imgNode, ttAjaxBinder, templates)

          renderList (head.getLeafs, head.getNode, _next, imgLeaf :: imgs, zs, ttAjaxBinder, templates)
        }
        else {
          val (imgNode, _next) = tail match {
            case head2 :: tail2 =>
              (
               <img src="/classpath/treetable/themes/default/tv-item.gif" class="ttimage"/>,
               Some (head2.getNode)
              )
            case Nil =>
              (
                <img src="/classpath/treetable/themes/default/tv-item-last.gif" class="ttimage"/>,
                next
              )
            }
              
          renderTr (head.getNode, parent, _next, zs, imgs, imgNode, ttAjaxBinder, templates)
        }

        renderList (tail, parent, next, imgs, zs, ttAjaxBinder, templates)
          
      case Nil => ()
    }
  }
  
  /**
   * render a tree-table using this jQuery plugin: http://www.hanpau.com/jquery/unobtrusivetreetable.php
   */
  
  def renderAjax [A <: AnyRef, B <: AnyRef] (treeModel : TreeModel [A, B],
                                           options : TreeTableOptions,
                                           ttAjaxHelpers : TtAjaxHelpers[Nothing] *)
    : (NodeSeq => NodeSeq) =
  {
    val ttAjaxBinder = new TtAjaxBinder (ttAjaxHelpers : _*)
    
    val func = new FuncCallback (ttAjaxBinder) 
    
    // process the tag <tr rowtype="_rowtype_"></tr>

    def transformRow (ns: Iterable[Node]): Seq[Node] = {

      // search the diferent row type in the xhtml (ns) and map their respective sub-template
      val templates = new HashMap [String, Node] ()

      func.templates = templates

      for (val tr <- ns) {
        tr match {
          case rowXml @ <tr>{ ns @ _* }</tr> if rowXml.attribute("rowtype").isDefined => 
                  rowXml.attribute("rowtype").elements.next match {
                      case Text (rowType) => templates += (rowType-> rowXml)
                      case _ => println ("rowtype attribute defined but with no value " + tr)
                  }
                  
          case _ => ()
        }
      }
      
      println (templates) 

      // process the in-memory row )the model)

      val zs = new NodeBuffer();
    
      renderList (treeModel.getLeafs, treeModel.getNode, None, Nil, zs, ttAjaxBinder, templates)

      zs

    }
    
    // TODO: tbody id="treetable" -> use the id of the templates or generate automatically one
    // Note: need to modify jqTreeTable.js first

    def transformOneNode (n: Node):Iterable[Node] = {
      n match {
        case x @ <tbody>{ ns @ _* }</tbody> =>
            <tbody id="treetable">{transformRow (ns)}</tbody>
        case Elem(namespace, label, attrs, scp, ns @ _*) =>
            Elem(namespace, label, attrs, scp, transform( ns ):_* )
        case z =>
            z 
      }
    }
    
    //
    def transform (xhtml : NodeSeq) : NodeSeq = {
      val zs = new NodeBuffer();
      for(val z <- xhtml) {zs &+ transformOneNode (z)}

      zs
    }
    
    //

    def renderJs () : String = {
    
      var sz = new StringBuilder (500)
    
      sz.append ( """
/* <![CDATA[ */		      
jQuery(function(){
    """)

      sz.append ("var urlLiftAjax='" + S.encodeURL (S.contextPath + "/" + LiftRules.ajaxPath) + "';\n")
      sz.append ("var key='" + S.mapFunc (new SFuncHolder (func.liftAjaxCallback)) + "';\n") ;
      sz.append ("var contextRoot='" + S.encodeURL (S.contextPath) + "';\n")
    
      sz.append ( """
    jQuery("#treetable").treetable(contextRoot, urlLiftAjax, key);
  })
/* ]]> */    
    """)

      sz.toString
    }
  
    //
    def ret (xhtml : NodeSeq) : NodeSeq = {
      val js = renderJs ()
    
      <xml:group>
      <head>
        <link href="/classpath/treetable/themes/default/treetable.css" type="text/css" rel="stylesheet" />
        <script type="text/javascript" src="/classpath/treetable/treetable.js"></script>
        
        <script type="text/javascript">
        {Unparsed (js)}
        </script>
      </head>
      {transform (xhtml)}
      </xml:group>
    }

    ret
  }
  
  // ===========================================================================
  // verion anterior basada en jqTreeTable (sin AJAX)
  // ===========================================================================

  /**
   * render a tree-table using this jQuery plugin: http://www.hanpau.com/jquery/unobtrusivetreetable.php
   */
  
  def render [A <: AnyRef, B <: AnyRef] (treeModel : TreeModel [A, B], options : TreeTableOptions, rowsBinder : Function2[NodeSeq, Nothing, NodeSeq]*)
    : (NodeSeq => NodeSeq) =
  {
    val binderMethod = new BinderMethod (rowsBinder : _*)
      
    // flatten the tree model
    val rows : List [Pair [AnyRef, NodeInfo]] = if (options.showRoot) treeModel.preorder else treeModel.preorder.drop (1)

    // process the tag <tr rowtype="_rowtype_"></tr>

    def transformRow (ns: Iterable[Node]): Seq[Node] = {
      // search the diferent row type in the xhtml (ns) and map their respective sub-template
      val templates = new HashMap [String, Node] ()

      for (val tr <- ns) {
        tr match {
          case rowXml @ <tr>{ ns @ _* }</tr> if rowXml.attribute("rowtype").isDefined => 
                  rowXml.attribute("rowtype").elements.next match {
                      case Text (rowType) => templates += (rowType-> rowXml)
                      case _ => println ("rowtype attribute defined but with no value " + tr)
                  }
                  
          case _ => ()
        }
      }

      // process the in-memory row )the model)

      val zs = new NodeBuffer();
    
      for (row <- rows)
      {
        // 
        val rowType = BinderMethod.rowTypeKey (row._1.getClass)

        // println (rowType)
        
        try {
          // search the row template
          val template = templates (rowType)
            
          // search the row-binder function
          val binder = binderMethod.get (rowType)

          val parameters = new Array [Object] (2)
          parameters (0) = template
          parameters (1) = row._1
          
          // invoke the row binder
          zs &+ binder._2.invoke (binder._1, parameters)
        }
        catch {
          case nseex : NoSuchElementException  => println ("Either template or binder not defined for row type " + rowType) ; nseex.printStackTrace () ; 
        }
      }

      zs
    }

    // TODO: tbody id="treetable" -> use the id of the templates or generate automatically one
    // Note: need to modify jqTreeTable.js first

    def transformOneNode (n: Node):Iterable[Node] = {
      n match {
        case x @ <tbody>{ ns @ _* }</tbody> =>
            <tbody id="treetable">{transformRow (ns)}</tbody>
        case Elem(namespace, label, attrs, scp, ns @ _*) =>
            Elem(namespace, label, attrs, scp, transform( ns ):_* )
        case z =>
            z 
      }
    }
    
    //

    def renderJsMap () : String = {
      def _generateMap (_rows : List [Pair [AnyRef, NodeInfo]]) : String = {
        _rows match {
          case List (last) => last._2.parentId.toString
          case head :: tail => head._2.parentId.toString + ", " + _generateMap (tail)
        }
      }
    
      _generateMap (rows)
    }

    //
    
    def _generateCollapse (_rows : List [Pair [Any, NodeInfo]]) : String = {
      _rows match {
        case Nil => ""
        case List (last) => last._2.id.toString
        case head :: tail => head._2.id.toString + ", " + _generateCollapse (tail)
      }
    }

    //
    
    def renderCollapse () : String = {
      val rowsToCollapse = rows.filter (row => row._2.isCollapsed) 
    
      // println (rowsToCollapse)
    
      rowsToCollapse match {
        case Nil => "false"
        case _ => "[" + _generateCollapse (rowsToCollapse) + "]" 
      }
    }
    
    //
    
    def renderCollapseLevel (level : Int) : String = {
      val rowsToCollapse = rows.filter (row => row._2.level == level) // jqQuery take care of sub-rows
    
      // println (rowsToCollapse)
    
      "[" + _generateCollapse (rowsToCollapse) + "]"
    }
    
    //

    def renderJs () : String = {
    
      var sz = new StringBuilder (500)
    
      sz.append ( """
/* <![CDATA[ */		      
jQuery(function(){
  var map = [ 
  """)
  
      sz.append (renderJsMap ())
  
      sz.append ("""
    ] ;
    var options = {
"""
      )
  
      sz.append ("      openImg: \""     + S.contextPath + "/classpath/treetable/themes/default/tv-collapsable.gif\",\n")  
      sz.append ("      shutImg: \""     + S.contextPath + "/classpath/treetable/themes/default/tv-expandable.gif\",\n")  
      sz.append ("      leafImg: \""     + S.contextPath + "/classpath/treetable/themes/default/tv-item.gif\",\n")
      sz.append ("      lastOpenImg: \"" + S.contextPath + "/classpath/treetable/themes/default/tv-collapsable-last.gif\",\n") 
      sz.append ("      lastShutImg: \"" + S.contextPath + "/classpath/treetable/themes/default/tv-expandable-last.gif\",\n")
      sz.append ("      lastLeafImg: \"" + S.contextPath + "/classpath/treetable/themes/default/tv-item-last.gif\",\n")
      sz.append ("      vertLineImg: \"" + S.contextPath + "/classpath/treetable/themes/default/vertline.gif\",\n")
      sz.append ("      blankImg: \""    + S.contextPath + "/classpath/treetable/themes/default/blank.gif\",\n")
  
      // 
      options.collapseUnderLevel match {
        case None => sz.append ("      collapse: " + renderCollapse () + ",\n")
        case Some(level) => sz.append ("      collapse: " + renderCollapseLevel (level) + ",\n")
      }
  
      sz.append ("      column: " + options.column + ",\n")        
      sz.append ("      striped: " + options.striped + ",\n")
  
      options.onselect match {
        case Some (function) => sz.append ("      onselect: " + function + ",\n")
        case None => 
      }
  
      sz.append ("      highlight: " + options.highlight + "\n")
      
      sz.append ("""  
    };
    jQuery("#treetable").jqTreeTable(map, options);
});
/* ]]> */    
    """)
    
      sz.toString
    }
  
    //
    def transform (xhtml : NodeSeq) : NodeSeq = {
      val zs = new NodeBuffer();
      for(val z <- xhtml) {zs &+ transformOneNode (z)}

      zs
    }
    
    //
    def ret (xhtml : NodeSeq) : NodeSeq = {
      val js = renderJs ()
    
      <xml:group>
      <head>
        <link href="/classpath/treetable/themes/default/jQTreeTable.css" type="text/css" rel="stylesheet" />
        <script type="text/javascript" src="/classpath/treetable/jQTreeTable.js"></script>
        
        <script type="text/javascript">
        {Unparsed (js)}
        </script>
      </head>
      {transform (xhtml)}
      </xml:group>
    }

    ret
  }
    
}
